// Copyright 2023 Touca, Inc. Subject to Apache-2.0 License.

#pragma once

/**
 * @file touca.hpp
 *
 * `touca/touca.hpp` is the only header file of Touca SDK for C++ that
 * you need to include in your regression test code. It provides all the
 * functions necessary to configure the core library, capture results and submit
 * them to the Touca server.
 */

#include <functional>

#include "touca/client/detail/options.hpp"
#include "touca/core/serializer.hpp"
#include "touca/extra/logger.hpp"
#include "touca/extra/scoped_timer.hpp"
#include "touca/lib_api.hpp"
#ifdef TOUCA_INCLUDE_RUNNER
#include "touca/runner/runner.hpp"
#endif

/**
 * @namespace touca
 *
 * @brief Provides an interface to the Touca SDK for C++.
 */
namespace touca {

/**
 * Attempts to configure the core Touca library.
 *
 * Must be called before declaring testcases or adding results to the client.
 *
 * @code
 *     touca::configure([](ClientOptions& x){
 *         x.api_key = "03dda763-62ea-436f-8395-f45296e56e4b"
 *         x.api_url = "https://api.touca.io"
 *         x.team = "acme",
 *         x.students = "students",
 *         x.version = "v1.0",
 *     });
 * @endcode
 *
 * Client is considered configured if it can capture test results and store
 * them locally on the filesystem. To do so, configuration options `team`,
 * `suite`, and `version` shall be provided, directly or indirectly, together
 * in a single call, or separately in a sequence of calls, in order for the
 * client to be considered as configured. These options may be specified, in
 * part or in full, as components of the configuration parameter `api-url`.
 *
 * In addition to the configuration parameters above, the parameters `api-url`
 * and `api-key` shall be provided for the client to be able to submit captured
 * test results to the server. As an example, the same configuration options
 * above could have been provided via the following code snippet.
 *
 * @code
 *     touca::configure([](ClientOptions& x){
 *         x.api_key = "03dda763-62ea-436f-8395-f45296e56e4b"
 *         x.api_url = "https://api.touca.io/@/acme/student/v1.0"
 *     });
 * @endcode
 *
 * If the API URL is missing and API Key is set, the API URL will be set to
 * https://api.touca.io.
 *
 * Since the function is designed to never throw and to allow users to
 * configure the client through a sequence of separate calls, we recommend
 * that you use `touca::is_configured()` to check if the client is configured
 * and learn about any potential error using `touca::configuration_error()`.
 *
 * @code
 *     if (!touca::is_configured()) {
 *         std::cerr << touca::configuration_error() << std::endl;
 *     }
 * @endcode
 *
 * @see `touca::ClientOptions` for a list of supported configuration
 * parameters
 *
 * @param options a callback function for setting configuration parameters
 */
TOUCA_CLIENT_API void configure(
    const std::function<void(ClientOptions&)> options = nullptr);

/**
 * Checks if the client is configured to perform basic operations including
 * capturing data points and storing them locally on the filesystem. Note that
 * when `api_key` and `api_url` are missing, the client is considered as
 * configured even though it cannot submit the test results to the server.
 *
 * @see `touca::configuration_error()` for extracting the most recent reason
 *      when `touca::configure()` is called but the client is not configured.
 *
 * @return true if the client is configured to perform basic operations.
 */
TOUCA_CLIENT_API bool is_configured();

/**
 * Provides the most recent error, if any, encountered during client
 * configuration.
 *
 * @see `touca::is_configured()` for checking if the client is configured before
 *      attempting to access and handle any configuration error.
 *
 * @return short description of the most recent configuration error
 */
TOUCA_CLIENT_API std::string configuration_error();

/**
 * Registers a custom logger that is notified when an event of potential
 * interest takes place.
 *
 * This function enables users to register their own logger derived from
 * `touca::logger` and listen for log events generated by the client library.
 * Log events include warnings and errors if client library is misused or fails
 * to perform an instructed action. Adding a logger is optional.
 *
 * @param logger a custom logger that is notified of log events generated by
 *               the core library.
 */
TOUCA_CLIENT_API void add_logger(const std::shared_ptr<touca::logger> logger);

/**
 * Declares the name of the testcase to which all subsequent results will be
 * submitted until a new testcase is declared.
 *
 * Unless configuration options `concurrency` is set to false, when a thread
 * calls `declare_testcase` all other threads also have their most recent
 * testcase changed to the newly declared one.
 *
 * @param name name of the testcase to be declared
 */
TOUCA_CLIENT_API void declare_testcase(const std::string& name);

/**
 * Removes all logged information associated with a given testcase.
 *
 * Removes from memory, all information that is logged for the previously
 * declared testcase, for all the threads, regardless of whether configuration
 * option `concurrency` is set.
 *
 * This function does not remove the test results results from the server,
 * in case they are already submitted. It clears all information about that
 * testcase from the client library such that switching back to an already
 * declared or already submitted testcase would behave similar to when that
 * testcase was first declared.
 *
 * Calling this function is useful in long-running regression tests, after
 * submission of the testcase to the server, if memory consumed by the client
 * library is a concern or if there is a risk that a future testcase with a
 * similar name may be executed.
 *
 * @param name name of the testcase to be removed from memory
 */
TOUCA_CLIENT_API void forget_testcase(const std::string& name);

#ifndef DOXYGEN_SHOULD_SKIP_THIS

/**
 * @namespace touca::detail
 *
 * @brief Provides functions internally used by client library.
 *
 * @details Directly calling functions exposed in this namespace
 *          is strongly discouraged.
 */
namespace detail {

TOUCA_CLIENT_API void check(const std::string& key, const data_point& value);

TOUCA_CLIENT_API void assume(const std::string& key, const data_point& value);

TOUCA_CLIENT_API void add_array_element(const std::string& key,
                                        const data_point& value);

}  // namespace detail

#endif  // DOXYGEN_SHOULD_SKIP_THIS

/**
 * Captures the value of a given variable as a test result for the declared
 * testcase and associates it with the specified key.
 *
 * Primary data capturing function for adding test results for the declared
 * testcase.
 *
 * @tparam Char type of string to be associated with the value stored as a
 *         result. Expected to be convertible to `std::basic_string<char>`.
 *
 * @tparam Value original type of value `value` to be stored as a result in
 *         association with the given key `key`.
 *
 * @param key name to be associated with the logged test result.
 *
 * @param value value to be logged as a test result.
 */
template <typename Char, typename Value>
void check(Char&& key, const Value& value) {
  touca::detail::check(std::forward<Char>(key),
                       serializer<Value>().serialize(value));
}

/**
 * Logs a given value as an assumption for the declared testcase and
 * associates it with the specified key.
 *
 * Assumptions are a special category of data points that are hardly ever
 * expected to change for a given test case between different versions of
 * the workflow.
 *
 * Assumptions are treated differently by the Touca server: The server
 * specially highlights assumptions if they are different between two test
 * versions and removes them from user focus if they remain unchanged.
 * Therefore, assumptions are particularly helpful for verifying assumptions
 * about input data and their properties.
 *
 * @tparam Char type of string to be associated with the value stored as an
 *         assumption. Expected to be convertible to `std::basic_string<char>`.
 *
 * @tparam Value original type of value `value` to be stored as
 *               an assumption in association with given key `key`.
 *
 * @param key name to be associated with the logged test result.
 *
 * @param value value to be logged as an assumption
 *
 * @see `touca::check()` as the primary data capturing function.
 */
template <typename Char, typename Value>
void assume(Char&& key, const Value& value) {
  touca::detail::assume(std::forward<Char>(key),
                        serializer<Value>().serialize(value));
}

/**
 * Adds a given data point as an element of an ordered set of data points
 * that are stored as one single test result entity for the testcase which
 * is associated with the specified key.
 *
 * Could be considered as a helper utility function. This method is
 * particularly helpful to log a list of elements as they are found:
 *
 * @code
 *     for (const auto number : numbers) {
 *         if (isPrime(number)) {
 *             touca::add_array_element("prime numbers", number);
 *             touca::add_hit_count("number of primes");
 *         }
 *     }
 * @endcode
 *
 * This pattern can be considered as a syntactic sugar for the following
 * alternative:
 *
 * @code
 *     std::vector<unsigned> primes;
 *     for (const auto number : numbers) {
 *         if (isPrime(number)) {
 *             primes.emplace_back(number);
 *         }
 *     }
 *     if (!primes.empty()) {
 *         touca::check("prime numbers", primes);
 *         touca::check("number of primes", primes.size());
 *     }
 * @endcode
 *
 * The items added to the list are not required to be of the
 * same type. The following code is acceptable:
 * @code
 *     touca::add_array_element("elements", 42);
 *     touca::add_array_element("elements", "forty three");
 * @endcode
 *
 * @tparam Char type of string to be associated with the value stored as an
 *         element. Expected to be convertible to `std::basic_string<char>`.
 * @tparam Value original type of value `value` to be stored as an element
 *         of an array associated with given key `key`.
 * @param key name to be associated with the logged test result.
 * @param value element to be appended to the array
 * @throw touca::detail::runtime_error if the specified key is already
 *        associated with a test result whose type is not a derivative of
 *        `touca::array`.
 * @see `touca::check()` as the primary data capturing function.
 * @since v1.1
 */
template <typename Char, typename Value>
void add_array_element(Char&& key, const Value& value) {
  touca::detail::add_array_element(std::forward<Char>(key),
                                   serializer<Value>().serialize(value));
}

/**
 * Increments the value of the data point associated with key `key`.
 * Creates the data point with the initial value of `1` if it does not exist.
 *
 * May be considered as a helper utility function. This method is particularly
 * helpful to track variables whose values are determined in loops with
 * indeterminate execution cycles:
 *
 * @code
 *     for (const auto number : numbers) {
 *         if (isPrime(number)) {
 *             add_array_element("prime numbers", number);
 *             add_hit_count("number of primes");
 *         }
 *     }
 * @endcode
 *
 * This pattern can be considered as a syntactic sugar for the following
 * alternative:
 *
 * @code
 *     std::vector<unsigned> primes;
 *     for (const auto number : numbers) {
 *         if (isPrime(number)) {
 *             primes.emplace_back(number);
 *         }
 *     }
 *     if (!primes.empty()) {
 *         touca::check("prime numbers", primes);
 *         touca::check("number of primes", primes.size());
 *     }
 * @endcode
 *
 * @param key name to be associated with the logged test result.
 * @throw touca::detail::runtime_error if the specified key is already
 *        associated with a test result which was not an integer.
 * @see `touca::check()` as the primary data capturing function.
 * @since v1.1
 */
TOUCA_CLIENT_API void add_hit_count(const std::string& key);

/**
 * Adds a performance measurement collected with the help of this library
 * as a performance benchmark (metric).
 *
 * @param key name to be associated with the performance metric
 * @param duration duration in number of milliseconds
 * @since v1.2.0
 */
TOUCA_CLIENT_API void add_metric(const std::string& key,
                                 const unsigned duration);

/**
 * Starts performance measurement for a given metric.
 *
 * Records the time of invocation of this function, associates it with the
 * given key and awaits a future call to `stop_timer` with the same key to
 * log the duration as a performance metric.
 *
 * @param key name to be associated with the performance metric
 * @since v1.1
 */
TOUCA_CLIENT_API void start_timer(const std::string& key);

/**
 * Stops performance measurement for a given metric.
 *
 * Logs a performance metric whose value is the duration between this call
 * and a previous call to `start_timer` with the same key.
 *
 * @param key name to be associated with the performance metric
 * @since v1.1
 */
TOUCA_CLIENT_API void stop_timer(const std::string& key);

/**
 * Stores the test results in binary format in a file with the specified path.
 *
 * Stores the test results assigned to given set of testcases in a file with
 * the specified path in binary format. We do not recommend as a general
 * practice for regression test tools to locally store their test results.
 * This feature may be helpful for special cases such as when test tools
 * have to be run in environments that have no access to the Touca server
 * (e.g. running with no network access).
 *
 * @param path path to file in which test results should be stored
 * @param testcases set of names of testcases whose results should be
 *                  stored. if given set is empty, all test cases will
 *                  be stored in the specified file.
 * @param overwrite determines whether to overwrite any file that exists
 *                  in the specified `path`. Defaults to **true**.
 */
TOUCA_CLIENT_API void save_binary(
    const std::string& path, const std::vector<std::string>& testcases = {},
    const bool overwrite = true);

/**
 * Stores test results in JSON format in a file with the specified path.
 *
 * Stores test results assigned to given set of testcases in a file with the
 * specified path in JSON format.
 *
 * @param path path to file in which test results should be stored
 * @param testcases set of names of testcases whose results should be
 *                  stored to disk. if given set is empty, all
 *                  testcases will be stored in the specified file.
 * @param overwrite determines whether to overwrite any file that exists
 *                  in the specified `path`. Defaults to **true**.
 */
TOUCA_CLIENT_API void save_json(const std::string& path,
                                const std::vector<std::string>& testcases = {},
                                const bool overwrite = true);

/**
 * Submits all test results recorded so far to Touca server.
 *
 * Posts all test results of all testcases declared by this client to Touca
 * server in flatbuffers format. Should only be called after the client is
 * configured.
 *
 * It is possible to call touca::post() multiple times during runtime of the
 * test tool. Test cases already submitted to the server whose test results
 * have not changed, will not be resubmitted. It is also possible to add test
 * results to a testcase after it is submitted to the server. Any subsequent
 * call to touca::post() will resubmit the modified testcase.
 *
 * @return enum indicating the status of submitted test results.
 * @throw touca::detail::runtime_error if client is not configured or that it
 *        is configured to operate without communicating with the server.
 */
TOUCA_CLIENT_API Post::Status post(const Post::Options& options = {});

/**
 * Notifies Touca server that all test cases were executed and no further
 * test result is expected to be submitted.
 *
 * Expected to be called by the test tool once all test cases are executed
 * and all test results are posted.
 *
 * Sealing the version is optional. The Touca server automatically performs
 * this operation once a certain amount of time has passed since the last
 * test case was submitted. This duration is configurable from the "Settings"
 * tab in "Suite" Page.
 *
 * @throw touca::detail::runtime_error if client is not configured or that it is
 *        configured to operate without communicating with the server.
 * @since v1.3
 */
TOUCA_CLIENT_API void seal();

}  // namespace touca
